/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.search;

import static org.hamcrest.core.StringContains.containsString;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.solr.CursorPagingTest;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.SolrParams;
import org.junit.AfterClass;
import org.junit.BeforeClass;

public class TestRandomCollapseQParserPlugin extends SolrTestCaseJ4 {

  /** Full SolrServer instance for arbitrary introspection of response data and adding fqs */
  public static SolrClient SOLR;

  public static List<String> ALL_SORT_FIELD_NAMES;
  public static List<String> ALL_COLLAPSE_FIELD_NAMES;

  private static final String[] NULL_POLICIES =
      new String[] {
        CollapsingQParserPlugin.NullPolicy.IGNORE.getName(),
        CollapsingQParserPlugin.NullPolicy.COLLAPSE.getName(),
        CollapsingQParserPlugin.NullPolicy.EXPAND.getName()
      };

  @BeforeClass
  public static void buildIndexAndClient() throws Exception {
    initCore("solrconfig-minimal.xml", "schema-sorts.xml");

    final int totalDocs = atLeast(500);
    for (int i = 1; i <= totalDocs; i++) {
      SolrInputDocument doc = CursorPagingTest.buildRandomDocument(i);
      // every doc will be in the same group for this (string) field
      doc.addField("same_for_all_docs", "xxx");
      assertU(adoc(doc));
    }
    assertU(commit());

    // Don't close this client, it would shut down the CoreContainer
    SOLR = new EmbeddedSolrServer(h.getCoreContainer(), h.coreName);

    ALL_SORT_FIELD_NAMES =
        CursorPagingTest.pruneAndDeterministicallySort(
            h.getCore().getLatestSchema().getFields().keySet());

    ALL_COLLAPSE_FIELD_NAMES = new ArrayList<>(ALL_SORT_FIELD_NAMES.size());
    for (String candidate : ALL_SORT_FIELD_NAMES) {
      if (candidate.startsWith("str")
          || candidate.startsWith("float")
          || candidate.startsWith("int")) {
        ALL_COLLAPSE_FIELD_NAMES.add(candidate);
      }
    }
  }

  @AfterClass
  public static void cleanupStatics() {
    SOLR = null;
    ALL_SORT_FIELD_NAMES = null;
    ALL_COLLAPSE_FIELD_NAMES = null;
  }

  public void testEveryIsolatedSortFieldOnSingleGroup() throws Exception {

    for (String sortField : ALL_SORT_FIELD_NAMES) {
      for (String dir : Arrays.asList(" asc", " desc")) {

        final String sort = sortField + dir + ", id" + dir; // need id for tiebreaker
        final String q = random().nextBoolean() ? "*:*" : CursorPagingTest.buildRandomQuery();

        final SolrParams sortedP = params("q", q, "rows", "1", "sort", sort);

        final QueryResponse sortedRsp = SOLR.query(sortedP);

        // random data -- might be no docs matching our query
        if (0 != sortedRsp.getResults().getNumFound()) {
          final SolrDocument firstDoc = sortedRsp.getResults().get(0);

          // check forced array resizing starting from 1
          for (String p : Arrays.asList("{!collapse field=", "{!collapse size='1' field=")) {
            for (String fq :
                Arrays.asList(
                    p + "same_for_all_docs sort='" + sort + "'}",
                    // nullPolicy=expand shouldn't change anything since every doc has the field
                    p + "same_for_all_docs sort='" + sort + "' nullPolicy=expand}",
                    // a field in no docs with nullPolicy=collapse should have had the same effect
                    // as
                    // collapsing on a field in every doc
                    p + "not_in_any_docs sort='" + sort + "' nullPolicy=collapse}")) {
              final SolrParams collapseP = params("q", q, "rows", "1", "fq", fq);

              // since every doc is in the same group, collapse query should return exactly one doc
              final QueryResponse collapseRsp = SOLR.query(collapseP);
              assertEquals(
                  "collapse should have produced exactly one doc: " + collapseP,
                  1,
                  collapseRsp.getResults().getNumFound());
              final SolrDocument groupHead = collapseRsp.getResults().get(0);

              // the group head from the collapse query should match the first doc of a simple sort
              assertEquals(
                  sortedP + " => " + firstDoc + " :VS: " + collapseP + " => " + groupHead,
                  firstDoc.getFieldValue("id"),
                  groupHead.getFieldValue("id"));
            }
          }
        }
      }
    }
  }

  public void testRandomCollapseWithSort() {

    final int numMainQueriesPerCollapseField = atLeast(5);

    for (String collapseField : ALL_COLLAPSE_FIELD_NAMES) {
      for (int i = 0; i < numMainQueriesPerCollapseField; i++) {

        final String topSort = CursorPagingTest.buildRandomSort(ALL_SORT_FIELD_NAMES);
        final String collapseSort = CursorPagingTest.buildRandomSort(ALL_SORT_FIELD_NAMES);

        final String q = random().nextBoolean() ? "*:*" : CursorPagingTest.buildRandomQuery();

        final SolrParams mainP = params("q", q, "fl", "id," + collapseField);

        final String csize =
            random().nextBoolean() ? "" : " size=" + TestUtil.nextInt(random(), 1, 10000);

        final String nullPolicy = randomNullPolicy();
        final String nullPs =
            nullPolicy.equals(CollapsingQParserPlugin.NullPolicy.IGNORE.getName())
                // ignore is default, randomly be explicit about it
                ? (random().nextBoolean() ? "" : " nullPolicy=ignore")
                : (" nullPolicy=" + nullPolicy);

        final SolrParams collapseP =
            params(
                "sort",
                topSort,
                "rows",
                "200",
                "fq",
                ("{!collapse"
                    + csize
                    + nullPs
                    + " field="
                    + collapseField
                    + " sort='"
                    + collapseSort
                    + "'}"));

        try {
          final QueryResponse mainRsp = SOLR.query(SolrParams.wrapDefaults(collapseP, mainP));

          for (SolrDocument doc : mainRsp.getResults()) {
            final Object groupHeadId = doc.getFieldValue("id");
            final Object collapseVal = doc.getFieldValue(collapseField);

            if (null == collapseVal) {
              if (nullPolicy.equals(CollapsingQParserPlugin.NullPolicy.EXPAND.getName())) {
                // nothing to check for this doc, it's in its own group
                continue;
              }

              assertNotEquals(
                  groupHeadId
                      + " has null collapseVal but nullPolicy==ignore; "
                      + "mainP: "
                      + mainP
                      + ", collapseP: "
                      + collapseP,
                  nullPolicy,
                  CollapsingQParserPlugin.NullPolicy.IGNORE.getName());
            }

            // workaround for SOLR-8082...
            //
            // what's important is that we already did the collapsing on the *real* collapseField
            // to verify the groupHead returned is really the best our verification filter
            // on docs with that value in a different field containing the exact same values
            final String checkField = collapseField.replace("float_dv", "float");

            final String checkFQ =
                ((null == collapseVal)
                    ? ("-" + checkField + ":[* TO *]")
                    : ("{!field f=" + checkField + "}" + collapseVal));

            final SolrParams checkP =
                params(
                    "fq", checkFQ,
                    "rows", "1",
                    "sort", collapseSort);

            final QueryResponse checkRsp = SOLR.query(SolrParams.wrapDefaults(checkP, mainP));

            assertFalse(
                "not even 1 match for sanity check query? expected: " + doc,
                checkRsp.getResults().isEmpty());
            final SolrDocument firstMatch = checkRsp.getResults().get(0);
            final Object firstMatchId = firstMatch.getFieldValue("id");
            assertEquals(
                "first match for filtered group '"
                    + collapseVal
                    + "' not matching expected group head ... "
                    + "mainP: "
                    + mainP
                    + ", collapseP: "
                    + collapseP
                    + ", checkP: "
                    + checkP,
                groupHeadId,
                firstMatchId);
          }
        } catch (Exception e) {
          throw new RuntimeException("BUG using params: " + collapseP + " + " + mainP, e);
        }
      }
    }
  }

  public void testParsedFilterQueryResponse() throws Exception {
    String nullPolicy = randomNullPolicy();
    String groupHeadSort = "'_version_ asc'";
    String collapseSize = "5000";
    String collapseHint = "top_fc";
    String filterQuery =
        "{!collapse field=id sort="
            + groupHeadSort
            + " nullPolicy="
            + nullPolicy
            + " size="
            + collapseSize
            + " hint="
            + collapseHint
            + "}";
    SolrParams solrParams = params("q", "*:*", "rows", "0", "debug", "true", "fq", filterQuery);

    QueryResponse response = SOLR.query(solrParams);
    // Query name is occurring twice, this should be handled in QueryParsing.toString
    String expectedParsedFilterString =
        "CollapsingPostFilter(CollapsingPostFilter(field=id, "
            + "nullPolicy="
            + nullPolicy
            + ", GroupHeadSelector(selectorText="
            + groupHeadSort.substring(1, groupHeadSort.length() - 1)
            + ", type=SORT"
            + "), hint="
            + collapseHint
            + ", size="
            + collapseSize
            + "))";
    List<String> expectedParsedFilterQuery = Collections.singletonList(expectedParsedFilterString);
    assertEquals(expectedParsedFilterQuery, response.getDebugMap().get("parsed_filter_queries"));
    assertEquals(
        Collections.singletonList(filterQuery), response.getDebugMap().get("filter_queries"));
  }

  public void testNullPolicy() {
    String nullPolicy = "xyz";
    String groupHeadSort = "'_version_ asc'";
    String filterQuery =
        "{!collapse field=id sort=" + groupHeadSort + " nullPolicy=" + nullPolicy + "}";
    SolrParams solrParams = params("q", "*:*", "fq", filterQuery);

    SolrException e = expectThrows(SolrException.class, () -> SOLR.query(solrParams));
    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
    assertThat(e.getMessage(), containsString("Invalid nullPolicy: " + nullPolicy));

    // valid nullPolicy
    assertQ(req("q", "*:*", "fq", "{!collapse field=id nullPolicy=" + randomNullPolicy() + "}"));
  }

  private String randomNullPolicy() {
    return NULL_POLICIES[TestUtil.nextInt(random(), 0, NULL_POLICIES.length - 1)];
  }
}
